@zz85によるresize&drag&snap scriptをcode readingする
readingだけではなくwritingもするつもりだが
ともかくどんな原理で動いているのかを調べる
2022-02-04
05:42:55 async-awaitで書き換えてみる
22:50:31 調査終了
snappingやresizeに対応していることを除けば、特段特別な仕組みを使っているわけではない
down→move→upの流れは変わらず
違うのはここかな
mouse deviceとtouch deviceとでevent処理を少しだけ変えている
code:index.ts
/*
*/
import { setBounds } from "./setBounds.ts";
// Minimum resizable area
const minWidth = 60;
const minHeight = 40;
// Thresholds
const FULLSCREEN_MARGINS = -10;
const MARGINS = 4;
// End of what's configurable.
code:setBounds.ts
export function setBounds(
element: HTMLElement,
x: number, y: number,
w: number, h: number
) {
element.style.left = ${x}px;
element.style.top = ${y}px;
element.style.width = ${w}px;
element.style.height = ${h}px;
}
code:index.ts
const ghostpane = document.getElementById("ghostpane")!;
function hintHide(b: DOMRect) {
setBounds(ghostpane, b.left, b.top, b.width, b.height);
ghostpane.style.opacity = 0;
}
event listenersの登録
mouse eventもtouch eventも同一のevent workflowonDown()→onMove()→onUp()を用いている
code:index.ts
const pane = document.getElementById("pane")!;
// Mouse events
pane.addEventListener("mousedown", (e) => {
onDown(e);
e.preventDefault();
});
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
// Touch events
pane.addEventListener("touchstart", (e) => {
e.preventDefault();
});
document.addEventListener("touchmove", (e) =>
);
document.addEventListener("touchend", (e) => {
// すべてのpointersが画面から離れたら終了とみなす
if (e.touches.length === 0) onUp(e.changedTouches0); });
pointerの各種情報
すべてのpointer eventで更新する
code:index.ts
let data: {
e: MouseEvent | Touch;
b: DOMRect;
x: number;
y: number;
/** pointerがコンテナの端にあるかどうか */
onRightEdge: boolean;
onBottomEdge: boolean;
onLeftEdge: boolean;
onTopEdge: boolean;
/** 画面の端との距離 */
rightScreenEdge: number;
bottomScreenEdge: number;
};
pointer down時のデータをclickedに格納する
code:index.ts
let clicked: {
x: number;
y: number;
cx: number;
cy: number;
w: number;
h: number;
state: "resize" | "drag" | "neutral";
onLeftEdge: boolean;
onTopEdge: boolean;
onRightEdge: boolean;
onBottomEdge: boolean;
} | { state: "neutral"; } = { state: "neutral" };
function onDown(e: MouseEvent | Touch) {
const {
x, y, b,
...rest
} = calc(e);
四端のいずれか付近でpointerを押下したときリサイズ処理に切り替える
code:index.ts
const isResizing = rest.onRightEdge ||
rest.onBottomEdge ||
rest.onTopEdge ||
rest.onLeftEdge;
data = { x, y, b, e, ...rest };
clicked = {
cx: e.clientX,
cy: e.clientY,
w: b.width,
h: b.height,
state: isResizing ? "resize" :
canMove(x, y, b) ? "drag" :
"neutral",
x,
y,
...rest
};
}
座表計算とスナップ及びリサイズの可否判定
code:index.ts
const canMove = (x: number, y: number, b) =>
x > 0 && x < b.width && y > 0 && y < b.height && y < 30;
function calc(e: MouseEvent | Touch) {
const b = pane.getBoundingClientRect();
const x = e.clientX - b.left;
const y = e.clientY - b.top;
return {
b,
x,
y,
onTopEdge: y < MARGINS,
onLeftEdge: x < MARGINS,
onRightEdge: x >= b.width - MARGINS,
onBottomEdge: y >= b.height - MARGINS,
rightScreenEdge: window.innerWidth - MARGINS,
bottomScreenEdge: window.innerHeight - MARGINS,
};
}
pointer moveで座標とポインタ情報を更新する
更新したときのみ再描画する
code:index.ts
let redraw = false;
function onMove(e: MouseEvent | Touch) {
data = { e, ...calc(e) };
redraw = true;
}
描画サイクル
無限ループ
code:index.ts
animate();
let preSnapped: {
width: number;
height: number;
} | null;
function animate() {
requestAnimationFrame(animate);
if (!redraw) return;
redraw = false;
const { x, y, b, e } = data;
switch (clicked?.state) {
case "resize": {
if (clicked.onRightEdge) pane.style.width = `${
Math.max(x, minWidth)
}px`;
if (clicked.onBottomEdge) pane.style.height = `${
Math.max(y, minHeight)
}px`;
if (clicked.onLeftEdge) {
const currentWidth = Math.max(clicked.cx - e.clientX + clicked.w, minWidth);
if (currentWidth > minWidth) {
pane.style.width = ${currentWidth}px;
pane.style.left = ${e.clientX}px;
}
}
if (clicked.onTopEdge) {
const currentHeight = Math.max(clicked.cy - e.clientY + clicked.h, minHeight);
if (currentHeight > minHeight) {
pane.style.height = ${currentHeight}px;
pane.style.top = ${e.clientY}px;
}
}
hintHide(b);
return;
}
case "drag": {
if (b.top < FULLSCREEN_MARGINS ||
b.left < FULLSCREEN_MARGINS ||
b.right > window.innerWidth - FULLSCREEN_MARGINS ||
b.bottom > window.innerHeight - FULLSCREEN_MARGINS
) {
// hintFull();
setBounds(ghostpane, 0, 0, window.innerWidth, window.innerHeight);
ghostpane.style.opacity = 0.2;
} else if (b.top < MARGINS) {
// hintTop();
setBounds(ghostpane, 0, 0, window.innerWidth, window.innerHeight / 2);
ghostpane.style.opacity = 0.2;
} else if (b.left < MARGINS) {
// hintLeft();
setBounds(ghostpane, 0, 0, window.innerWidth / 2, window.innerHeight);
ghostpane.style.opacity = 0.2;
} else if (b.right > rightScreenEdge) {
// hintRight();
setBounds(ghostpane, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight);
ghostpane.style.opacity = 0.2;
} else if (b.bottom > bottomScreenEdge) {
// hintBottom();
setBounds(ghostpane, 0, window.innerHeight / 2, window.innerWidth, window.innerWidth / 2);
ghostpane.style.opacity = 0.2;
} else {
hintHide(b);
}
// 前回スナップしていた場合は移動量計算を変える
if (preSnapped) {
setBounds(pane,
e.clientX - preSnapped.width / 2,
e.clientY - Math.min(clicked.y, preSnapped.height),
preSnapped.width,
preSnapped.height
);
return;
}
// moving
pane.style.top = ${e.clientY - clicked.y}px;
pane.style.left = ${e.clientX - clicked.x}px;
return;
}
}
カーソルのアイコンはevent phaseに依らずカーソルとコンテナ・windowとの位置関係のみで決める
code:index.ts
// This code executes when mouse moves without clicking
// style cursor
const {
onLeftEdge,
onTopEdge,
onRightEdge,
onBottomEdge,
} = data;
pane.style.cursor =
onRightEdge && onBottomEdge || onLeftEdge && onTopEdge ?
"nwse-resize" :
onRightEdge && onTopEdge || onBottomEdge && onLeftEdge ?
"nesw-resize" :
onRightEdge || onLeftEdge ?
"ew-resize" :
onBottomEdge || onTopEdge ?
"ns-resize" :
canMove(x, y, b) ?
"move" :
"default";
}
pointerを離したときの処理
code:index.ts
function onUp(e: MouseEvent | Touch) {
data = calc(e);
const { b } = data;
code:index.ts
// Snap
if (clicked?.state === "drag") {
const snapped = {
width: b.width,
height: b.height
};
if (b.top < FULLSCREEN_MARGINS || b.left < FULLSCREEN_MARGINS || b.right > window.innerWidth - FULLSCREEN_MARGINS || b.bottom > window.innerHeight - FULLSCREEN_MARGINS) {
// hintFull();
setBounds(pane, 0, 0, window.innerWidth, window.innerHeight);
preSnapped = snapped;
} else if (b.top < MARGINS) {
// hintTop();
setBounds(pane, 0, 0, window.innerWidth, window.innerHeight / 2);
preSnapped = snapped;
} else if (b.left < MARGINS) {
// hintLeft();
setBounds(pane, 0, 0, window.innerWidth / 2, window.innerHeight);
preSnapped = snapped;
} else if (b.right > rightScreenEdge) {
// hintRight();
setBounds(pane, window.innerWidth / 2, 0, window.innerWidth / 2, window.innerHeight);
preSnapped = snapped;
} else if (b.bottom > bottomScreenEdge) {
// hintBottom();
setBounds(pane, 0, window.innerHeight / 2, window.innerWidth, window.innerWidth / 2);
preSnapped = snapped;
} else {
preSnapped = null;
}
hintHide(b);
}
// 押下状態を解除
clicked.state = "neutral";
}
code:html
<div id="pane">
<div id="title">Resize, Drag or Snap Me!</div>
</div>
<div id="ghostpane"></div>
code:css
body {
overflow: hidden;
}
position: absolute;
width: 45%;
height: 45%;
top: 20%;
left: 20%;
margin: 0;
padding: 0;
z-index: 99;
border: 2px solid purple;
}
font-family: monospace;
background: purple;
color: white;
font-size: 24px;
height: 30px;
text-align: center;
}
opacity: 0.2;
width: 45%;
height: 45%;
top: 20%;
left: 20%;
position: absolute;
margin: 0;
padding: 0;
z-index: 98;
transition: all 0.25s ease-in-out;
}